/*******************************************************************************
 * Copyright (c) 2014 Francesco Gabbrielli (http://www.francescogabbrielli.it).
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Francesco Gabbrielli - initial API and implementation
 ******************************************************************************/
package it.francescogabbrielli.android.solidalapp;

import it.francescogabbrielli.android.solidalapp.EMF;

import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;
import com.google.api.server.spi.response.CollectionResponse;
import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.datanucleus.query.JPACursorHelper;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import javax.annotation.Nullable;
import javax.inject.Named;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityNotFoundException;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;

@Api(name = "requestendpoint", namespace = @ApiNamespace(ownerDomain = "francescogabbrielli.it", ownerName = "francescogabbrielli.it", packagePath = "android.solidalapp"))
public class RequestEndpoint extends MessageEndpoint {
	
	/**
	 * This method lists all the entities inserted in datastore.
	 * It uses HTTP GET method and paging support.
	 *
	 * @return A CollectionResponse class containing the list of all entities
	 * persisted and a cursor to the next page.
	 */
	@SuppressWarnings({"unchecked"})
	@ApiMethod(name = "listRequest", path="list")
	public CollectionResponse<Request> listRequest(
			@Named("deviceId") Long deviceId,
			@Nullable @Named("cursor") String cursorString,
			@Nullable @Named("limit") Integer limit) {

		EntityManager mgr = null;
		Cursor cursor = null;
		List<Request> execute = null;
		List<Request> filtered = new LinkedList<Request>();

		try {
			mgr = getEntityManager();
			Query query = mgr.createQuery(
					"select from Request as Request "
						+ "and canceled = false "
						+ "and completed = false "
						+ "order by timestamp desc"
					);
				
			if (cursorString != null && cursorString != "") {
				cursor = Cursor.fromWebSafeString(cursorString);
				query.setHint(JPACursorHelper.CURSOR_HINT, cursor);
			}
			
			if (limit==null)
				limit = 50;
			
			if (limit != null) {
				query.setFirstResult(0);
				query.setMaxResults(limit);
			}

			execute = (List<Request>) query.getResultList();
			cursor = JPACursorHelper.getCursor(execute);
			if (cursor != null)
				cursorString = cursor.toWebSafeString();

			// Tight loop for fetching all entities from datastore and accomodate
			// for lazy fetch.
			// Added filtering
			for (Request req: execute) {
				complete(req);
				if (!req.isExpired() && req.getDeviceId()!=deviceId.longValue())
					filtered.add(req);
			}
				
		} finally {
			mgr.close();
		}
		
		logger.info("Requests: "+filtered.size()+ " out of " +execute.size() + " loaded");

		return CollectionResponse.<Request> builder().setItems(filtered)
				.setNextPageToken(cursorString).build();
	}

	/**
	 * This method gets the last request for a given device
	 *
	 * @param deviceId
	 * 					the device id
	 * @return The entity 
	 */
	@ApiMethod(name = "getLast", path="last")
	public Request getLast(@Named("deviceId") Long deviceId) {
		EntityManager mgr = getEntityManager();
		List<Request> result = null;
		try {
			Query query = mgr.createQuery(
					"select from Request as Request "
//					+ "where timestamp < :now "
					+ "where canceled = false "
//					+ "and completed = false "
					+ "and deviceId = :deviceId "
					+ "order by timestamp desc");
			query.setParameter("deviceId", deviceId);
//			query.setParameter("now", System.currentTimeMillis());
			query.setMaxResults(1);
			@SuppressWarnings({"unchecked"})
			List<Request> r = (List<Request>) query.getResultList();
			result = r;
		} finally {
			mgr.close();
		}
		return result==null || result.isEmpty() ? null : complete(result.get(0));
	}
	
	/**
	 * This method gets the entity having primary key id. It uses HTTP GET method.
	 *
	 * @param id the primary key of the java bean.
	 * @return The entity with primary key id.
	 */
	@ApiMethod(name = "getRequest")
	public Request getRequest(@Named("id") Long id) {
		EntityManager mgr = getEntityManager();
		Request request = null;
		try {
			request = mgr.find(Request.class, id);
		} finally {
			mgr.close();
		}
		return complete(request);
	}

	/**
	 * <p>
	 * This inserts a new request into App Engine datastore. If the entity already
	 * exists in the datastore, an exception is thrown.
	 * 
	 * <p>
	 * Then sends it to the GCM server for multicast dispatching to the registered 
	 * devices (up to 1000) that result to be located in the neighbourhood of
	 * the request.
	 * 
	 * <p>
	 * It uses HTTP POST method.
	 *
	 * @param request the entity to be inserted.
	 * @return The inserted entity.
	 */
	@ApiMethod(name = "insertRequest")
	public Request insertRequest(Request request) {
		
		EntityManager mgr = getEntityManager();
		EntityTransaction tx = mgr.getTransaction();
		
		long id = 0;
		request.setError(null);
		request.setTimestamp(System.currentTimeMillis());
		request.setExpiry(request.getTimestamp() + request.getDuration() * 60000L);
		
		try {
			
			if(request.getId()!=null)
				throw new EntityExistsException("Request already exists? "+request.getMessage());
			
			tx.begin();
			mgr.persist(request);
			tx.commit();
			
			id = request.getId();

		} catch(Exception e) {
						
			logger.severe("Error inserting request: " + e);
			request.setError("Server error: "+e.getMessage());
			return request;
			
		} finally {
			
			if (tx.isActive() && request.getError()!=null) {
				logger.warning("Rollbacking: "+tx);
				tx.rollback();
			}
			
			mgr.close();
		}
		
		try {
			CollectionResponse<Device> devices = deviceEndpoint.listDevice(null, null);
			Collection<Device> items = devices.getItems();
			Collection<Device> allowed = null;
//			if (items.size()<100) {
				allowed = items;
//			} else {
//				allowed = new LinkedList<Device>();
//				for (Device device : items)
//					if (device.getId().longValue()!=request.getDeviceId())
//						if (Math.abs(device.getLatitude()-request.getLatitude())*122d < 2)
//							if (Math.abs(device.getLongitude()-request.getLongitude())*122d < 2)
//								allowed.add(device);
//			}
			if (allowed!=null && !allowed.isEmpty()) {
				logger.info("Request inserted; now dispatching to GCM "+allowed.size()+" devices");
				doSendViaGcm(request, allowed);
			} else {
				logger.info("No device to dispatch");
				throw new Exception("No registered devices around");
			}
		} catch(Exception e) {
			request.setError("Error: "+e.getMessage());
			logger.severe("Error sending gcm multicast: " + e);
			try {
				if(id>0)
					removeRequest(id);
			} catch(Throwable t) {
				logger.severe("Error removing bad request: " + t);
			}
		}
		
		return request;
	}

	/**
	 * This method is used for updating an existing entity. If the entity does not
	 * exist in the datastore, an exception is thrown.
	 * It uses HTTP PUT method.
	 *
	 * @param request the entity to be updated.
	 * @return The updated entity.
	 */
	@ApiMethod(name = "updateRequest")
	public Request updateRequest(Request request) {
		EntityManager mgr = getEntityManager();
		try {
			Request item = mgr.find(Request.class, request.getId());
			if (item==null) {
				throw new EntityNotFoundException("Request not found");
			} else if (item.isAccepted()) {
				throw new IllegalArgumentException("Request already accepter");
			}
			logger.info("Update request: "+request);
			mgr.persist(request);
		} finally {
			mgr.close();
		}
		return request;
	}

	/**
	 * This method removes the entity with primary key id.
	 * It uses HTTP DELETE method.
	 *
	 * @param id the primary key of the entity to be deleted.
	 */
	@ApiMethod(name = "removeRequest")
	public void removeRequest(@Named("id") Long id) {
		EntityManager mgr = getEntityManager();
		try {
			Request request = mgr.find(Request.class, id);
			mgr.remove(request);
		} finally {
			mgr.close();
		}
	}

	private static EntityManager getEntityManager() {
		return EMF.get().createEntityManager();
	}
	
	/**
	 * Complete the request prior to serving back to clients
	 * 
	 * @param request
	 * 		the request to complete 
	 * @return
	 * 		the same request
	 */
	private Request complete(Request request) {
		if (request!=null) {
			long diff = System.currentTimeMillis() - request.getExpiry();
			if (!request.isAccepted())
				request.setExpired(diff>0);
			else if (diff> 86400l * 1000l)
				request.setExpired(true);
		}
		return request;
	}
	
}
